﻿// Copyright (c) 2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

// This source file provides elementary support for reading and writing 
// Flash SWF files. 
// 
// A nice and thorough SWF encoder/decoder implementation is available at
// http://sourceforge.net/projects/swfdotnet/. However, that project uses
// GPL license, which forbids its inclusion in our library.

using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO.Compression;
using System.Runtime.InteropServices;
using Util.IO;
using Util.IO.Compression;
using Util.Media;

namespace Util.Media.Flash
{
    public class FlashFile
    {
        private List<Tag> tags = new List<Tag>();

        public FileHeader Header { get; set; }
        public Tag[] Tags { get { return tags.ToArray(); } }

        public FlashFile()
        {
        }

        public FlashFile(Stream stream)
        {
            this.Header = FlashReader.ReadHeaderFirstHalf(stream);
            if (Header.IsCompressed)
                stream = new ZLibStream(stream, CompressionMode.Decompress);

            using (FlashReader reader = new FlashReader(stream))
            {
                reader.ReadHeaderSecondHalf(this.Header);
                while (true)
                {
                    Tag tag = Tag.Deserialize(reader);
                    tags.Add(tag);
                    if (tag is EndTag)
                        break;
                }
            }
        }

        public void AddTags(IEnumerable<Tag> tags)
        {
            this.tags.AddRange(tags);
        }

        public void Save(Stream output)
        {
            using (FlashWriter writer = new FlashWriter(output))
            {
                // Write first half of header.
                byte[] signature = new byte[]
                {
                    (byte)(Header.IsCompressed? 'C':'F'),
                    (byte)'W',
                    (byte)'S',
                    Header.Version
                };
                writer.Write(signature);

                // Entire file length... we don't know yet, just put
                // some number.
                writer.Write((uint)100);

                // FrameSize RECT... 
                writer.WriteRectangleF(Header.FrameBounds);
                writer.WriteFixed8(Header.FrameRate);
                writer.Write((ushort)Header.FrameCount);

                // Write tags.
                foreach (Tag tag in tags)
                {
                    tag.Serialize(writer);
                }

                // Now we know the file length. Let's write it back.
                writer.BaseStream.Seek(4, SeekOrigin.Begin);
                writer.Write((int)writer.BaseStream.Length);
            }
        }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class FileHeader
    {
        [Description("Whether the file is compressed using ZLib compression.")]
        [DefaultValue(false)]
        public bool IsCompressed { get; set; }

        [Description("SWF version.")]
        public byte Version { get; set; }

        [Description("Size of the file in bytes after decompression.")]
        public uint FileSize { get; set; }

        [Description("Frame bounds in pixels.")]
        [Browsable(false)]
        public RectangleF FrameBounds { get; set; }

        [Description("Number of frames played per second.")]
        public float FrameRate { get; set; }

        [Description("Number of frames in the file.")]
        public ushort FrameCount { get; set; }

        [Description("Frame width in pixels.")]
        public float FrameWidth { get { return FrameBounds.Width; } }

        [Description("Frame height in pixels.")]
        public float FrameHeight { get { return FrameBounds.Height; } }
    }

    [TypeConverter(typeof(ExpandableObjectConverter))]
    public class FrameDefinition
    {
        [Description("Frame bounds in pixels.")]
        [Browsable(false)]
        public RectangleF FrameBounds { get; set; }

        [Description("Number of frames played per second.")]
        public float FrameRate { get; set; }

        [Description("Number of frames in the file.")]
        public ushort FrameCount { get; set; }
    }

    public class FlashReader : BinaryReader
    {
        public FlashReader(Stream stream) : base(stream) { }

        /// <summary>
        /// Buffer from which to read bit values. The least significant
        /// bitsBufferSize bits of bitsBuffer contain valid data.
        /// </summary>
        private long bitsBuffer = 0;

        /// <summary>
        /// Number of bits available in bitsBuffer. These are the least
        /// significant bits.
        /// </summary>
        private int bitsBufferSize = 0;

        /// <summary>
        /// Whether the reader is currently in BitValue reading mode.
        /// </summary>
        private bool bitsMode = false;

        /// <summary>
        /// Set the reader in BitValue reading mode. In this mode, the user
        /// is allowed to call ReadS/U/FBits() function. We require the user
        /// to call EnterBitValueMode() and LeaveBitValueMode() explicitly
        /// to avoid unwanted redundant bits being carried over to later
        /// fields. Ideally, we should also forbid the other Read* methods
        /// to be called in BitValue mode, but that will be too much work.
        /// </summary>
        public void EnterBitValueMode()
        {
            if (bitsMode)
                throw new InvalidOperationException("The reader is already in BitValue mode.");
            bitsMode = true;
            bitsBufferSize = 0; // just to be safe
        }

        public void LeaveBitValueMode()
        {
            if (!bitsMode)
                throw new InvalidOperationException("The reader is not in BitValue mode.");
            bitsMode = false;
            bitsBufferSize = 0; // discard unused bits
        }

        public int ReadSBits(uint numBits)
        {
            if (numBits < 0 || numBits > 32)
                throw new ArgumentOutOfRangeException("numBits");
            if (!bitsMode)
                throw new InvalidOperationException("This method can only be called in BitValue mode.");
            if (numBits == 0)
                return 0;

            while (numBits > bitsBufferSize)
            {
                byte b = base.ReadByte(); // skip BitValueMode checking in ReadByte
                bitsBuffer <<= 8;
                bitsBuffer |= b;
                bitsBufferSize += 8;
            }
            int val = (int)(bitsBuffer << (64 - bitsBufferSize) >> (64 - (int)numBits));
            bitsBufferSize -= (int)numBits;
            return val;
        }

        public uint ReadUBits(uint numBits)
        {
            return (uint)(ReadSBits(numBits) & ((1 << (int)numBits) - 1));
        }

        public float ReadFBits(uint numBits)
        {
            return ReadSBits(numBits) / 65536.0f;
        }

        public float ReadFixed8()
        {
            ushort x = ReadUInt16();
            return (float)x / 256;
        }

        public override string ReadString()
        {
            List<byte> bytes = new List<byte>(100);
            byte b;
            while ((b = ReadByte()) != 0)
                bytes.Add(b);
            return Encoding.UTF8.GetString(bytes.ToArray());
        }

        /// <summary>
        /// Reads a RECT record from the stream. Since a RECT record must
        /// be byte aligned, the caller must call LeaveBitValueMode() first
        /// prior to calling this function if it is in BitValue mode.
        /// </summary>
        public RectangleF ReadRectangleF()
        {
            EnterBitValueMode();

            uint nbits = ReadUBits(5);
            if (nbits == 0)
                throw new InvalidDataException();

            float xMin = ReadSBits(nbits) / 20.0f;
            float xMax = ReadSBits(nbits) / 20.0f;
            float yMin = ReadSBits(nbits) / 20.0f;
            float yMax = ReadSBits(nbits) / 20.0f;

            LeaveBitValueMode();
            return new RectangleF(xMin, yMin, xMax - xMin, yMax - yMin);
        }

        /// <summary>
        /// Reads a MATRIX record from the stream. Since a MATRIX record must
        /// be byte aligned, the caller must call LeaveBitValueMode() first
        /// prior to calling this function if it is in BitValue mode.
        /// </summary>
        public Matrix ReadMatrix()
        {
            EnterBitValueMode();

            float m11 = 1.0f, m22 = 1.0f;
            bool hasScale = (ReadUBits(1) != 0);
            if (hasScale)
            {
                uint nbits = ReadUBits(5);
                m11 = ReadFBits(nbits);
                m22 = ReadFBits(nbits);
            }

            float m12 = 0.0f, m21 = 0.0f;
            bool hasRotate = (ReadUBits(1) != 0);
            if (hasRotate)
            {
                uint nbits = ReadUBits(5);
                m12 = ReadFBits(nbits);
                m21 = ReadFBits(nbits);
            }

            float dx, dy;
            if (true)
            {
                uint nbits = ReadUBits(5);
                dx = ReadSBits(nbits) / 20.0f;
                dy = ReadSBits(nbits) / 20.0f;
            }

            LeaveBitValueMode();
            return new Matrix(m11, m12, m21, m22, dx, dy);
        }

        public static FileHeader ReadHeaderFirstHalf(Stream stream)
        {
            FileHeader header = new FileHeader();

            byte[] buffer = new byte[8];
            stream.ReadFull(buffer, 0, buffer.Length);

            if (buffer[0] == 'F')
                header.IsCompressed = false;
            else if (buffer[0] == 'C')
                header.IsCompressed = true;
            else
                throw new InvalidDataException("Signature mismatch.");
            if (!(buffer[1] == 'W' && buffer[2] == 'S'))
                throw new InvalidDataException("Signature mismatch.");

            header.Version = buffer[3];
            header.FileSize = BitConverter.ToUInt32(buffer, 4);
            return header;
        }

        public void ReadHeaderSecondHalf(FileHeader header)
        {
            header.FrameBounds = ReadRectangleF();
            header.FrameRate = ReadFixed8();
            header.FrameCount = ReadUInt16();
        }

        public Color ReadRGB()
        {
            byte r = ReadByte();
            byte g = ReadByte();
            byte b = ReadByte();
            return Color.FromArgb(255, r, g, b);
        }

        public Color ReadRGBA()
        {
            byte r = ReadByte();
            byte g = ReadByte();
            byte b = ReadByte();
            byte a = ReadByte();
            return Color.FromArgb(a, r, g, b);
        }

        public Color ReadARGB()
        {
            byte a = ReadByte();
            byte r = ReadByte();
            byte g = ReadByte();
            byte b = ReadByte();
            return Color.FromArgb(a, r, g, b);
        }
    }

    public class FlashWriter : BinaryWriter
    {
        public FlashWriter(Stream stream) : base(stream) { }

        /// <summary>
        /// Buffer onto which to write bit values. The least significant
        /// bitsBufferSize bits of bitsBuffer contain valid data.
        /// </summary>
        private ulong bitsBuffer = 0;

        /// <summary>
        /// Number of bits available in bitsBuffer. These are the least
        /// significant bits.
        /// </summary>
        private int bitsBufferSize = 0;

        /// <summary>
        /// Whether the writer is currently in BitValue reading mode.
        /// </summary>
        private bool bitsMode = false;

        /// <summary>
        /// Set the writer in BitValue writing mode. In this mode, the user
        /// is allowed to call WriteS/U/FBits() functions. We require the user
        /// to call EnterBitValueMode() and LeaveBitValueMode() explicitly
        /// to avoid forgetting to flush bits. Ideally, we should also forbid
        /// the other Write* methods to be called in BitValue mode, but that 
        /// will be too much work.
        /// </summary>
        public void EnterBitValueMode()
        {
            if (bitsMode)
                throw new InvalidOperationException("The reader is already in BitValue mode.");
            bitsMode = true;
            bitsBufferSize = 0; // just to be safe
        }

        private void FlushBitsBuffer(bool force)
        {
            while (bitsBufferSize >= 8)
            {
                Write((byte)(bitsBuffer >> (bitsBufferSize - 8)));
                bitsBufferSize -= 8;
            }
            if (bitsBufferSize > 0 && force) // zero-pad and flush remaining bits
            {
                Write((byte)(bitsBuffer << (8 - bitsBufferSize)));
                bitsBufferSize = 0;
            }
        }

        public void LeaveBitValueMode()
        {
            if (!bitsMode)
                throw new InvalidOperationException("The reader is not in BitValue mode.");
            bitsMode = false;

            if (bitsBufferSize > 0)
                FlushBitsBuffer(true);
        }

        public void WriteSBits(int value, uint numBits)
        {
            if (numBits <= 0 || numBits > 32)
                throw new ArgumentOutOfRangeException("numBits");
            if ((value >> (int)numBits) != 0 && (value >> (int)numBits) != -1)
                throw new ArgumentOutOfRangeException("value", "value cannot be represented in numBits bits.");
            if (!bitsMode)
                throw new InvalidOperationException("This method can only be called in BitValue mode.");

            bitsBuffer <<= (int)numBits;
            bitsBuffer |= (uint)(value & ((1 << (int)numBits) - 1));
            bitsBufferSize += (int)numBits;
            FlushBitsBuffer(false);
        }

        public void WriteUBits(uint value, uint numBits)
        {
            WriteSBits((int)value, numBits);
        }

        public void WriteFBits(float value, uint numBits)
        {
            WriteSBits((int)(value * 65536.0f), numBits);
        }

        public void WriteFixed8(float value)
        {
            Write((ushort)(value * 256.0f));
        }

        public override void Write(string value)
        {
            byte[] bytes = Encoding.UTF8.GetBytes(value);
            base.Write(bytes, 0, bytes.Length);
            base.Write((byte)0);
        }

        /// <summary>
        /// Writes a RECT record from the stream. Since a RECT record must
        /// be byte aligned, the caller must call LeaveBitValueMode() first
        /// prior to calling this function if it is in BitValue mode.
        /// </summary>
        public void WriteRectangleF(RectangleF rect)
        {
            EnterBitValueMode();

            int xMin = (int)Math.Round(rect.Left * 20.0f);
            int xMax = (int)Math.Round(rect.Right * 20.0f);
            int yMin = (int)Math.Round(rect.Top * 20.0f);
            int yMax = (int)Math.Round(rect.Bottom * 20.0f);

            uint nbits = 31; // Let's be simplistic here...
            WriteUBits(nbits, 5);
            WriteSBits(xMin, nbits);
            WriteSBits(xMax, nbits);
            WriteSBits(yMin, nbits);
            WriteSBits(yMax, nbits);

            LeaveBitValueMode();
        }

        /// <summary>
        /// Writes a MATRIX record to the stream. Since a MATRIX record must
        /// be byte aligned, the caller must call LeaveBitValueMode() first
        /// prior to calling this function if it is in BitValue mode.
        /// </summary>
        public void WriteMatrix(Matrix m)
        {
            EnterBitValueMode();

            float m11 = m.Elements[0];
            float m12 = m.Elements[1];
            float m21 = m.Elements[2];
            float m22 = m.Elements[3];
            float dx = m.Elements[4]; // shall multiply by 20.0
            float dy = m.Elements[5]; // shall multiply by 20.0

            if (m11 != 1.0f || m22 != 1.0f) // HasScale
            {
                WriteUBits(1, 1);
                uint nbits = 31;
                WriteUBits(nbits, 5);
                WriteFBits(m11, nbits);
                WriteFBits(m22, nbits);
            }
            else
            {
                WriteUBits(0, 1);
            }

            if (m12 != 0.0f || m21 != 0.0f) // HasRotate
            {
                WriteUBits(1, 1);
                uint nbits = 31;
                WriteUBits(nbits, 5);
                WriteFBits(m12, nbits);
                WriteFBits(m21, nbits);
            }
            else
            {
                WriteUBits(0, 1);
            }

            if (true)
            {
                uint nbits = 31;
                WriteUBits(nbits, 5);
                WriteSBits((int)(dx * 20.0f), nbits);
                WriteSBits((int)(dy * 20.0f), nbits);
            }

            LeaveBitValueMode();
        }

#if false
        public static FileHeader ReadHeaderFirstHalf(Stream stream)
        {
            FileHeader header = new FileHeader();

            byte[] buffer = new byte[8];
            stream.ReadFull(buffer, 0, buffer.Length);

            if (buffer[0] == 'F')
                header.IsCompressed = false;
            else if (buffer[0] == 'C')
                header.IsCompressed = true;
            else
                throw new InvalidDataException("Signature mismatch.");
            if (!(buffer[1] == 'W' && buffer[2] == 'S'))
                throw new InvalidDataException("Signature mismatch.");

            header.Version = buffer[3];
            header.FileSize = BitConverter.ToUInt32(buffer, 4);
            return header;
        }

        public void ReadHeaderSecondHalf(FileHeader header)
        {
            header.FrameBounds = ReadRectangleF();
            header.FrameRate = ReadFixed8();
            header.FrameCount = ReadUInt16();
        }
#endif

        public void WriteRGB(Color value)
        {
            Write(new byte[] { value.R, value.G, value.B });
        }

        public void WriteRGBA(Color value)
        {
            Write(new byte[] { value.R, value.G, value.B, value.A });
        }

        public void WriteARGB(Color value)
        {
            Write(new byte[] { value.A, value.R, value.G, value.B });
        }
    }
}
